/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define LOG_TAG "BAHAL"
// #define LOG_NDEBUG 0

#include <errno.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <log/log.h>
#include <cutils/str_parms.h>

#include "ext_pcm.h"

static pthread_mutex_t ext_pcm_init_lock = PTHREAD_MUTEX_INITIALIZER;

#ifdef BAHAL_SUPPORT_DYNAMIC_BIT_FORMAT
static int gBitWidth;
#endif

static struct ext_pcm* shared_ext_pcm_bus_media = NULL;
static struct ext_pcm* shared_ext_pcm_bus_sys_notification = NULL;
static struct ext_pcm* shared_ext_pcm_bus_navi = NULL;

static struct ext_pcm* get_ext_pcm_by_device (int card_id, int device_id){
    if (PCM_CARD_VIRTIO_DEFAULT == card_id) {
      if(PCM_DEVICE_BUS00_MEDIA == device_id) return shared_ext_pcm_bus_media;
      else if(PCM_DEVICE_BUS01_SYS_NOTIFICATION == device_id) return shared_ext_pcm_bus_sys_notification;
      else if(PCM_DEVICE_BUS02_NAV_GUIDANCE == device_id) return shared_ext_pcm_bus_navi;
    }
    return shared_ext_pcm_bus_media;
}

static void reset_ext_pcm_by_device (int card_id, int device_id){
    if (PCM_CARD_VIRTIO_DEFAULT == card_id) {
      if(PCM_DEVICE_BUS00_MEDIA == device_id) shared_ext_pcm_bus_media = NULL;
      else if(PCM_DEVICE_BUS01_SYS_NOTIFICATION == device_id) shared_ext_pcm_bus_sys_notification = NULL;
      else if(PCM_DEVICE_BUS02_NAV_GUIDANCE == device_id) shared_ext_pcm_bus_navi = NULL;
    }
    ALOGE("%s: no matching ext_pcm for %d:%d", __func__, card_id, device_id);
    return;;
}


// Sleep 10ms between each mixing, this interval value is arbitrary chosen
#define MIXER_INTERVAL_MS 10
#define MS_TO_US 1000

#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#define MIN(a, b) (((a) < (b)) ? (a) : (b))

/* copied from libcutils/str_parms.c */
static bool str_eq(void *key_a, void *key_b) {
  return !strcmp((const char *)key_a, (const char *)key_b);
}

/**
 * use djb hash unless we find it inadequate.
 * copied from libcutils/str_parms.c
 */
#ifdef __clang__
__attribute__((no_sanitize("integer")))
#endif
static int str_hash_fn(void *str) {
  uint32_t hash = 5381;
  char *p;
  for (p = str; p && *p; p++) {
    hash = ((hash << 5) + hash) + *p;
  }
  return (int)hash;
}

static bool mixer_thread_mix(__unused void *key, void *value, void *context) {
  struct ext_mixer_pipeline *pipeline_out = (struct ext_mixer_pipeline *)context;
  struct ext_mixer_pipeline *pipeline_in = (struct ext_mixer_pipeline *)value;
  pipeline_out->position = MAX(pipeline_out->position, pipeline_in->position);
  for (int i = 0; i < pipeline_out->position; i++) {
#ifdef BAHAL_SUPPORT_DYNAMIC_BIT_FORMAT
    uint8_t* out = pipeline_out->buffer + i * gBitWidth;
    uint8_t* in = pipeline_in->buffer + i * gBitWidth;
    //TODO print out, in ,max, -max.
    if(sizeof(int32_t) == gBitWidth) {
      int32_t max = INT32_MAX;
      float mixed = *((int32_t*)out) + *((int32_t*)in);
      if (mixed > max) *((int32_t*)out) = max;
      else if (mixed < INT32_MIN) *((int32_t*)out) = INT32_MIN;
      else {
        *((int32_t*)out) = (int32_t)mixed;
      }
    } else if(sizeof(int16_t) == gBitWidth){
      int16_t max = INT16_MAX;
      int mixed = *((int16_t*)out) + *((int16_t*)in);
      if (mixed > max) *((int16_t*)out) = max;
      else if (mixed < INT16_MIN) *((int16_t*)out) = INT16_MIN;
      else {
        *((int16_t*)out) = (int16_t)mixed;
      }
    }
#else
    if(sizeof(BUFFER_ITEM_TYPE) == sizeof(int16_t)) {
        float mixed = pipeline_out->buffer[i] + pipeline_in->buffer[i];
        if (mixed > INT16_MAX) pipeline_out->buffer[i] = INT16_MAX;
        else if (mixed < INT16_MIN) pipeline_out->buffer[i] = INT16_MIN;
        else pipeline_out->buffer[i] = (int16_t)mixed;
    } else if (sizeof(BUFFER_ITEM_TYPE) == sizeof(int32_t)) {
      float mixed = pipeline_out->buffer[i] + pipeline_in->buffer[i];
      if (mixed > INT32_MAX) pipeline_out->buffer[i] = INT32_MAX;
      else if (mixed < INT32_MIN) pipeline_out->buffer[i] = INT32_MIN;
      else pipeline_out->buffer[i] = (int32_t)mixed;
    }
#endif
  }
  memset(pipeline_in, 0, sizeof(struct ext_mixer_pipeline));
  return true;
}

static int merge_data_to_channel(char* out_buffer, const char* in_buffer, int src_size,
                                   int src_channels, int dest_channels, int bit_width) {
    int i = 0;
    int copy_channels = src_channels;
    if (src_channels > dest_channels) copy_channels = dest_channels;
    int loop_count = src_size / bit_width / src_channels;

    for(; i < loop_count; i ++) {
        memcpy(out_buffer + i * dest_channels * bit_width,
               in_buffer + i * bit_width * src_channels,
               bit_width * copy_channels);
    }
    return src_size * dest_channels / src_channels;
}

static void *mixer_thread_loop(void *context) {
  char thread_name[32] = {0};
  struct ext_pcm *ext_pcm = (struct ext_pcm *)context;
  sprintf(thread_name, "%s-%d:%d", "car_mixer_loop", ext_pcm->card_id, ext_pcm->device_id);
  pthread_setname_np(pthread_self(), thread_name);
  ALOGD("%s: starting mixer loop for pcm %d:%d", __func__, ext_pcm->card_id, ext_pcm->device_id);
  char* ext_write_buffer = NULL;
  int ext_pcm_write_buffer_size = ext_pcm->period_count * pcm_frames_to_bytes(ext_pcm->pcm, ext_pcm->period_size);
  ext_write_buffer = (char*)malloc(ext_pcm_write_buffer_size);
  if(NULL == ext_write_buffer) {
    ALOGE("%s: failed to allocate %d bytes", __func__, ext_pcm_write_buffer_size);
    return NULL;
  } else {
    ALOGD("%s: allocated %d bytes", __func__, ext_pcm_write_buffer_size);
  }

  do {
    pthread_mutex_lock(&ext_pcm->mixer_lock);
    ext_pcm->mixer_pipeline.position = 0;
    // Combine the output from every pipeline into one output buffer
    hashmapForEach(ext_pcm->mixer_pipeline_map, mixer_thread_mix,
        &ext_pcm->mixer_pipeline);
    if (ext_pcm->mixer_pipeline.position > 0) {
      int dest_bytes = 0;
      int src_bytes = ext_pcm->mixer_pipeline.position * ext_pcm->bit_width;
      if(ext_pcm->src_channels != ext_pcm->dest_channels) {
          dest_bytes = merge_data_to_channel(ext_write_buffer, (const char*)(ext_pcm->mixer_pipeline.buffer),
                               src_bytes, ext_pcm->src_channels, ext_pcm->dest_channels, ext_pcm->bit_width);
      } else {
          memcpy(ext_write_buffer, (const char*)(ext_pcm->mixer_pipeline.buffer), src_bytes);
          dest_bytes = src_bytes;
      }
      //TODO print when several seconds
      //ALOGD("%s ch: %d -> %d, src_bytes=%d, dest_bytes=%d. position=%d", __func__, ext_pcm->src_channels, ext_pcm->dest_channels, src_bytes, dest_bytes, ext_pcm->mixer_pipeline.position);
      int ret = pcm_write(ext_pcm->pcm, (void*)ext_write_buffer, dest_bytes);
      if (ret != 0) {
        ALOGE("%s error[%d] %s: writing data to pcm", __func__, ret, ext_pcm_get_error(ext_pcm));
      }
    }
    memset(&ext_pcm->mixer_pipeline, 0, sizeof(struct ext_mixer_pipeline));
    pthread_cond_broadcast(&ext_pcm->mixer_wake);
    pthread_mutex_unlock(&ext_pcm->mixer_lock);
    pthread_mutex_lock(&ext_pcm_init_lock);
    bool keep_running = ext_pcm->run_mixer;
    pthread_mutex_unlock(&ext_pcm_init_lock);
    if (!keep_running) {
      break;
    }
    usleep(MIXER_INTERVAL_MS * MS_TO_US);
  } while (1);
  if(NULL != ext_write_buffer) free(ext_write_buffer);
  ALOGD("%s: exiting mixer loop", __func__);
  return NULL;
}

static int mixer_pipeline_write(struct ext_pcm *ext_pcm, const char *bus_address,
                                const void *data, unsigned int count) {
  pthread_mutex_lock(&ext_pcm->mixer_lock);
  struct ext_mixer_pipeline *pipeline = hashmapGet(
      ext_pcm->mixer_pipeline_map, bus_address);
  if (!pipeline) {
    pipeline = calloc(1, sizeof(struct ext_mixer_pipeline));
    ALOGI("%s: allocate pipeline. bit_width=%d", __func__, ext_pcm->bit_width);
    hashmapPut(ext_pcm->mixer_pipeline_map, bus_address, pipeline);
  }
  unsigned int byteWritten = 0;
  bool write_incomplete = true;
  do {
    const unsigned int byteCount = MIN(count - byteWritten,
        (MIXER_FRAME_BUFFER_SIZE * ext_pcm->src_channels / ext_pcm->dest_channels - pipeline->position) 
        * (ext_pcm->bit_width));
    const unsigned int frameCount = byteCount / (ext_pcm->bit_width);
    if (frameCount > 0) {
#ifdef BAHAL_SUPPORT_DYNAMIC_BIT_FORMAT
        memcpy(&pipeline->buffer[pipeline->position * (ext_pcm->bit_width)], (const char*)data + byteWritten, byteCount);
#else
        memcpy(&pipeline->buffer[pipeline->position], (const char*)data + byteWritten, byteCount);
#endif
        pipeline->position += frameCount;
    }
    byteWritten += byteCount;
    write_incomplete = byteWritten < count;
    if (write_incomplete) {
      // wait for mixer thread to consume the pipeline buffer
      pthread_cond_wait(&ext_pcm->mixer_wake, &ext_pcm->mixer_lock);
    }
  } while (write_incomplete);
  pthread_mutex_unlock(&ext_pcm->mixer_lock);
  return 0;
}

struct ext_pcm *ext_pcm_open(unsigned int card, unsigned int device,
                             unsigned int flags, struct pcm_config *config, int src_channels) {
  pthread_mutex_lock(&ext_pcm_init_lock);
  struct ext_pcm* shared_ext_pcm = get_ext_pcm_by_device(card, device);
  if (shared_ext_pcm == NULL) {
    ALOGD("%s: create ext_pcm for device %d:%d", __func__, card, device);
    shared_ext_pcm = calloc(1, sizeof(struct ext_pcm));
    pthread_mutex_init(&shared_ext_pcm->lock, (const pthread_mutexattr_t *) NULL);
    shared_ext_pcm->pcm = pcm_open(card, device, flags, config);
    pthread_mutex_init(&shared_ext_pcm->mixer_lock, (const pthread_mutexattr_t *)NULL);
    pthread_create(&shared_ext_pcm->mixer_thread, (const pthread_attr_t *)NULL,
            mixer_thread_loop, shared_ext_pcm);
    shared_ext_pcm->mixer_pipeline_map = hashmapCreate(8, str_hash_fn, str_eq);
    shared_ext_pcm->run_mixer = true;
  }
  shared_ext_pcm->ref_count += 1;
  pthread_mutex_unlock(&ext_pcm_init_lock);

  shared_ext_pcm->card_id = card;
  shared_ext_pcm->device_id = device;
  shared_ext_pcm->dest_channels = config->channels;
  shared_ext_pcm->src_channels = src_channels;
  shared_ext_pcm->period_size =  config->period_size;
  shared_ext_pcm->period_count =  config->period_count;
  shared_ext_pcm->bit_width = ext_get_bit_width_from_format(config->format);
  #ifdef BAHAL_SUPPORT_DYNAMIC_BIT_FORMAT
  gBitWidth = shared_ext_pcm->bit_width;
  #endif
  ALOGI("%s: created ext_pcm. channel[%d->%d] bit_width:%d", __func__,
        shared_ext_pcm->src_channels, shared_ext_pcm->dest_channels, shared_ext_pcm->bit_width);
  return shared_ext_pcm;
}

static bool mixer_free_pipeline(__unused void *key, void *value, void *context) {
  struct ext_mixer_pipeline *pipeline = (struct ext_mixer_pipeline *)value;
  free(pipeline);
  return true;
}

int ext_pcm_close(struct ext_pcm *ext_pcm) {
  ALOGD("%s closing pcm", __func__);
  if (ext_pcm == NULL || ext_pcm->pcm == NULL) {
    return -EINVAL;
  }

  pthread_mutex_lock(&ext_pcm_init_lock);
  int count = ext_pcm->ref_count -= 1;
  if (count <= 0) {
    ext_pcm->run_mixer = false;
    // On pcm open new shared_ext_pcm will be created
    reset_ext_pcm_by_device(ext_pcm->card_id, ext_pcm->device_id);
    pthread_mutex_unlock(&ext_pcm_init_lock);
    void* ret_val = NULL;
    int ret = pthread_join(ext_pcm->mixer_thread, &ret_val);
    if (ret != 0) {
      ALOGE("%s error[%d] when joining thread",
        __func__, ret);
      // Try killing if timeout failed
      pthread_kill(ext_pcm->mixer_thread, SIGINT);
    }
    pthread_mutex_lock(&ext_pcm_init_lock);
    pthread_mutex_destroy(&ext_pcm->lock);
    pcm_close(ext_pcm->pcm);
    pthread_mutex_destroy(&ext_pcm->mixer_lock);
    hashmapForEach(ext_pcm->mixer_pipeline_map, mixer_free_pipeline,
        (void *)NULL);
    hashmapFree(ext_pcm->mixer_pipeline_map);
    free(ext_pcm);
  }
  pthread_mutex_unlock(&ext_pcm_init_lock);
  ALOGD("%s finished closing pcm", __func__);
  return 0;
}

int ext_pcm_is_ready(struct ext_pcm *ext_pcm) {
  if (ext_pcm == NULL || ext_pcm->pcm == NULL) {
    return 0;
  }

  return pcm_is_ready(ext_pcm->pcm);
}

int ext_pcm_write(struct ext_pcm *ext_pcm, const char *address,
                  const void *data, unsigned int count) {
  if (ext_pcm == NULL || ext_pcm->pcm == NULL) {
    return -EINVAL;
  }

  return mixer_pipeline_write(ext_pcm, address, data, count);
}

const char *ext_pcm_get_error(struct ext_pcm *ext_pcm) {
  if (ext_pcm == NULL || ext_pcm->pcm == NULL) {
    return NULL;
  }

  return pcm_get_error(ext_pcm->pcm);
}

int ext_get_bit_width_from_format(enum pcm_format format) {
    int bits = pcm_format_to_bits(format);
    return bits / 8;
}

unsigned int ext_pcm_frames_to_bytes(struct ext_pcm *ext_pcm, struct pcm_config* local_config,
                                     unsigned int frames) {
  if (ext_pcm == NULL || ext_pcm->pcm == NULL) {
    return -EINVAL;
  }
  int bit_width = ext_get_bit_width_from_format(local_config->format);
  unsigned int frame_bytes = 
           frames * bit_width * local_config->channels;
  //return pcm_frames_to_bytes(ext_pcm->pcm, frames);
  //ALOGD("%s local size=%u, ext pcm size=%u", __func__, frame_bytes, pcm_frames_to_bytes(ext_pcm->pcm, frames));
  return frame_bytes;
}

